package factory.one.service;

import factory.one.utils.SqlUtil;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.StringJoiner;

/**
 * @author sk.z
 * 2023/8/1
 * 用户服务
 **/
@Service
public class UserService implements UserDetailsService {

    private final JdbcTemplate jdbcTemplate;
    private final PasswordEncoder passwordEncoder;

    public UserService(JdbcTemplate jdbcTemplate,
                       PasswordEncoder passwordEncoder) {
        this.jdbcTemplate = jdbcTemplate;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        List<UserDetails> list = jdbcTemplate.query("select user_name, password, authorities, enabled from users where user_name = ?", this::mapToUser, username);
        if (CollectionUtils.isEmpty(list)) {
            throw new UsernameNotFoundException("user not found.");
        }
        return list.get(0);
    }

    public List<UserDetails> list(int page, int size) {
        return jdbcTemplate.query("select user_name, password, authorities, enabled from users " + SqlUtil.page(page, size), this::mapToUser);
    }

    public Integer listCount() {
        return jdbcTemplate.queryForObject("select count(0) from users ", Integer.class);
    }

    public void createUser(final UserDetails user) {
        Assert.hasText(user.getUsername(), "Username may not be empty or null");
        validateAuthorities(user.getAuthorities());

        jdbcTemplate.update("insert into users (user_name, password, authorities, enabled) values (?,?,?,?)", (ps) -> {
            ps.setString(1, user.getUsername());
            ps.setString(2, passwordEncoder.encode(user.getPassword()));
            ps.setString(3, authoritiesToString(user.getAuthorities()));
            ps.setBoolean(4, true);
        });
    }

    public void updateUser(final UserDetails user) {
        Assert.hasText(user.getUsername(), "Username may not be empty or null");
        validateAuthorities(user.getAuthorities());

        jdbcTemplate.update("update users set password = ?, authorities=?, enabled = ? where user_name = ?", (ps) -> {
            ps.setString(1, passwordEncoder.encode(user.getPassword()));
            ps.setString(2, authoritiesToString(user.getAuthorities()));
            ps.setBoolean(3, true);
            ps.setString(4, user.getUsername());
        });
    }

    public void updateUserAuthority(final UserDetails user) {
        Assert.hasText(user.getUsername(), "Username may not be empty or null");
        validateAuthorities(user.getAuthorities());

        jdbcTemplate.update("update users set authorities=?, enabled = ? where user_name = ?", (ps) -> {
            ps.setString(1, authoritiesToString(user.getAuthorities()));
            ps.setBoolean(2, true);
            ps.setString(3, user.getUsername());
        });
    }

    public void deleteUser(String userName) {
        jdbcTemplate.update("delete from users where user_name = ?", userName);
    }

    public void changePassword(String oldPassword, String newPassword) {
        Authentication currentUser = SecurityContextHolder.getContext().getAuthentication();
        if (currentUser == null) {
            throw new AccessDeniedException("Can't change password as no Authentication object found in context for current user.");
        }
        String userName = currentUser.getName();
        if (!passwordEncoder.matches(oldPassword, loadUserByUsername(userName).getPassword())) {
            throw new AccessDeniedException("old password is wrong.");
        }
        newPassword = passwordEncoder.encode(newPassword);
        jdbcTemplate.update("update users set password = ? where user_name = ?", newPassword, userName);

        Authentication authentication = createNewAuthentication(currentUser, newPassword);
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(authentication);
        SecurityContextHolder.setContext(context);
    }

    private UserDetails mapToUser(ResultSet rs, int rowNum) throws SQLException {
        return User.withUsername(rs.getString(1))
                .password(rs.getString(2))
                .roles(rs.getString(3).split(","))
                .disabled(!rs.getBoolean(4))
                .build();
    }

    protected Authentication createNewAuthentication(Authentication currentAuth, String newPassword) {
        UserDetails user = loadUserByUsername(currentAuth.getName());
        UsernamePasswordAuthenticationToken newAuthentication = UsernamePasswordAuthenticationToken.authenticated(user,
                null, user.getAuthorities());
        newAuthentication.setDetails(currentAuth.getDetails());
        return newAuthentication;
    }

    private String authoritiesToString(Collection<? extends GrantedAuthority> authorities) {
        StringJoiner stringJoiner = new StringJoiner(",");
        for (GrantedAuthority authority : authorities) {
            String value = authority.getAuthority();
            if (value.startsWith("ROLE_")) {
                stringJoiner.add(value.substring(5));
            } else {
                stringJoiner.add(value);
            }
        }
        return stringJoiner.toString();
    }

    private void validateAuthorities(Collection<? extends GrantedAuthority> authorities) {
        Assert.notNull(authorities, "Authorities list must not be null");
        Iterator iterator = authorities.iterator();
        while (iterator.hasNext()) {
            GrantedAuthority authority = (GrantedAuthority) iterator.next();
            Assert.notNull(authority, "Authorities list contains a null entry");
            Assert.hasText(authority.getAuthority(), "getAuthority() method must return a non-empty string");
        }
    }
}
